#!/usr/bin/env python
# utils/update-checkout - Utility to update your local checkouts -*- python -*-
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors

from __future__ import print_function

import argparse
import json
import os
import sys

from functools import reduce

sys.path.append(os.path.dirname(__file__))

from SwiftBuildSupport import (
    SWIFT_SOURCE_ROOT,
)  # noqa (E402 module level import not at top of file)

SCRIPT_FILE = os.path.abspath(__file__)
SCRIPT_DIR = os.path.dirname(SCRIPT_FILE)

sys.path.append(os.path.join(SCRIPT_DIR, 'swift_build_support'))

from swift_build_support import shell  # noqa (E402)


def update_single_repository(repo_path, branch):
    if not os.path.isdir(repo_path):
        return

    print("--- Updating '" + repo_path + "' ---")
    with shell.pushd(repo_path, dry_run=False, echo=False):
        if branch:
            status = shell.capture(['git', 'status', '--porcelain', '-uno'],
                                   echo=False)
            if status:
                print("Please, commit your changes.")
                print(status)
                exit(1)
            shell.call(['git', 'checkout', branch], echo=False)

        # Prior to Git 2.6, this is the way to do a "git pull
        # --rebase" that respects rebase.autostash.  See
        # http://stackoverflow.com/a/30209750/125349
        shell.call(["git", "fetch"], echo=False)
        shell.call(["git", "rebase", "FETCH_HEAD"], echo=False)
        shell.call(["git", "submodule", "update", "--recursive"],
                   echo=False)


def update_all_repositories(args, config, scheme_name):
    repo_branch = scheme_name
    for repo_name in config['repos'].keys():
        if repo_name in args.skip_repository_list:
            print("--- Skipping '" + repo_name + "' ---")
            continue
        if scheme_name:
            # This loop is only correct, since we know that each alias set has
            # unique contents. This is checked by verify config. Thus the first
            # branch scheme data that has scheme_name as one of its aliases is
            # the only possible correct answer.
            for v in config['branch-schemes'].values():
                if scheme_name not in v['aliases']:
                    continue
                repo_branch = v['repos'][repo_name]
                break

        update_single_repository(os.path.join(SWIFT_SOURCE_ROOT, repo_name),
                                 repo_branch)


def obtain_additional_swift_sources(
        config, with_ssh, scheme_name, skip_history, skip_repository_list):
    with shell.pushd(SWIFT_SOURCE_ROOT, dry_run=False,
                     echo=False):
        for repo_name, repo_info in config['repos'].items():
            if repo_name in skip_repository_list:
                print("--- Skipping '" + repo_name + "' ---")
                continue

            if os.path.isdir(os.path.join(repo_name, ".git")):
                continue

            print("--- Cloning '" + repo_name + "' ---")

            # If we have a url override, use that url instead of
            # interpolating.
            remote_repo_info = repo_info['remote']
            if 'url' in remote_repo_info:
                remote = remote_repo_info['url']
            else:
                remote_repo_id = remote_repo_info['id']
                if with_ssh is True or 'https-clone-pattern' not in config:
                    remote = config['ssh-clone-pattern'] % remote_repo_id
                else:
                    remote = config['https-clone-pattern'] % remote_repo_id

            if skip_history:
                shell.call(['git', 'clone', '--recursive', '--depth', '1',
                            remote, repo_name], echo=False)
            else:
                shell.call(['git', 'clone', '--recursive', remote,
                            repo_name], echo=False)
            if scheme_name:
                for v in config['branch-schemes'].values():
                    if scheme_name not in v['aliases']:
                        continue
                    repo_branch = v['repos'][repo_name]
                    break
                else:
                    repo_branch = scheme_name
                src_path = os.path.join(SWIFT_SOURCE_ROOT, repo_name,
                                        ".git")
                shell.call(['git', '--git-dir', src_path, '--work-tree',
                            os.path.join(SWIFT_SOURCE_ROOT, repo_name),
                            'checkout', repo_branch], echo=False)
    with shell.pushd(os.path.join(SWIFT_SOURCE_ROOT, repo_name),
                     dry_run=False, echo=False):
        shell.call(["git", "submodule", "update", "--recursive"],
                   echo=False)


def validate_config(config):
    # Make sure that our branch-names are unique.
    scheme_names = config['branch-schemes'].keys()
    if len(scheme_names) != len(set(scheme_names)):
        raise RuntimeError('Configuration file has duplicate schemes?!')

    # Then make sure the alias names used by our branches are unique.
    #
    # We do this by constructing a list consisting of len(names),
    # set(names). Then we reduce over that list summing the counts and taking
    # the union of the sets. We have uniqueness if the length of the union
    # equals the length of the sum of the counts.
    data = [(len(v['aliases']), set(v['aliases']))
            for v in config['branch-schemes'].values()]
    result = reduce(lambda acc, x: (acc[0] + x[0], acc[1] | x[1]), data,
                    (0, set([])))
    if result[0] == len(result[1]):
        return
    raise RuntimeError('Configuration file has schemes with duplicate '
                       'aliases?!')


def main():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description="""
repositories.

By default, updates your checkouts of Swift, SourceKit, LLDB, and SwiftPM.""")
    parser.add_argument(
        "--clone",
        help="Obtain Sources for Swift and Related Projects",
        action="store_true")
    parser.add_argument(
        "--clone-with-ssh",
        help="Obtain Sources for Swift and Related Projects via SSH",
        action="store_true")
    parser.add_argument(
        "--skip-history",
        help="Skip histories when obtaining sources",
        action="store_true")
    parser.add_argument(
        "--skip-repository",
        metavar="DIRECTORY",
        default=[],
        help="Skip the specified repository",
        dest='skip_repository_list',
        action="append")
    parser.add_argument(
        "--branch",
        help='Obtain Sources for specific branch',
        metavar='BRANCH',
        dest='scheme')
    parser.add_argument(
        "--config",
        default=os.path.join(SCRIPT_DIR, "update-checkout-config.json"),
        help="Configuration file to use")
    args = parser.parse_args()

    clone = args.clone
    clone_with_ssh = args.clone_with_ssh
    skip_history = args.skip_history
    scheme = args.scheme

    with open(args.config) as f:
        config = json.load(f)
    validate_config(config)

    if clone or clone_with_ssh:
        # If branch is None, default to using the default branch alias specified by
        # our configuration file.
        if scheme is None:
            scheme = config['default-branch-scheme']

        obtain_additional_swift_sources(
            config, clone_with_ssh, scheme, skip_history,
            args.skip_repository_list)

    update_all_repositories(args, config, scheme)

    return 0

if __name__ == "__main__":
    sys.exit(main())
